# 第6章 代码分片

code splitting 可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。
代码分片可以有效降低首屏加载资源的大小。

# CommonsChunkPlugin

CommonsChunkPlugin是webpack4之前内部自带的插件(4之后替换为了SplitChunks)。它可以将多个Chunk中公共的部分提取出来

  1. 开发过程中减少了重复模块打包,可以提升开发速度。
  2. 减小整体资源体积。
  3. 合理分片后的代码可以更有效的利用客户端缓存。
const webpack = require('webpack');
module.exports = {
    entry: {
        foo: './foo.js',
        bar: './bar.js',
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'commons', // 公共chunk的名字
            filename: 'commons.js' // 资源的文件名
        })
    ]
}

// 打包后有三个文件
// foo.js bar.js commons.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 提取vendor

单入口也可以使用CommonsChunkPlugin。可以用它来提取第三方类库及业务中不常更新的模块,只需要单独为它们创建一个入口。

const webpack = require('webpack');
module.exports = {
    entry: {
        app: './app.js',
        vendor: ['react'],
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 公共chunk的名字
            filename: 'vendor.js' // 资源的文件名
        }) 
        
         n  
    ]
}

// 打包后
// app.js vendor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在插件内部配置中,将name指定为vendor,这样由CommonsCHunkPlugin所产生的资源将覆盖原有的由vendor这个入口所产生的资源。

# 设置提取范围

通过CommonsChunkPlugin中的chunks配置项可以规定从哪些入口中提取公共模块。

const webpack = require('webpack');
module.exports = {
    entry: {
        a: './a.js',
        b: './b.js',
        c: './c.js',
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'commons', // 公共chunk的名字
            filename: 'commons.js', // 资源的文件名
            chunks: ['a', 'b']
        }) 
    ]
}

// 打包后
// a.js b.js c.js commons.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Asset Size Chunks Chunk Names
b.js 503 bytes 0 b
a.js 503 bytes 1 a
c.js 72.1 kb 2, 3 c
commons.js 73 kb 3 commons

# 设置提取规则

有时候我们不希望所有的公共模块都被提取出来,虽然被多次引用,但是可能经常修改,如果将其和react这种库放在一起反而不利于客户端缓存。
此时可以通过minChunks配置项来设置提取规则。

# 数字

minChunks可以接受一个数字,当设置minChunks为n时,只有该模块被n个入口同时引用时才会进行提取,这个值不会影响通过数组形式入口传入模块的提取

const webpack = require('webpack');
module.exports = {
    entry: {
        foo: './foo.js',
        bar: './bar.js',
        vendor: ['react']
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 公共chunk的名字
            filename: 'vendor.js', // 资源的文件名
            minChunks: 3
        }) 
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// foo.js
import React from 'react';
import './util';

// bar.js
import React from 'react';
import './util';

// util.js
console.log('util');
1
2
3
4
5
6
7
8
9
10

由于minChunks为3,utils.js并不会被提取到vendor.js中,然而react并不受这个的影响,仍然会出现在vendor.js中。这就是数组形式入口的模块会照常提取。

# Infinity

设置为无穷代表提取的值无线高,也就是说所有模块都不会被提取。

  1. 我们只想让webpack提取特定的几个模块,并将这些模块通过数组型入口传入。
  2. 为了生成一个没有任何模块而仅仅包含webpack初始化环境的文件,这个文件我们称为manifest。

# 函数

webpack打包过程中的每个模块都会经过这个函数的处理,当函数的返回值为true时进行提取。

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: 'vendor.js',
    minChunks: function(module, count) {
        // module.context 模块目录路径
        if(module.context && module.context.includes('mode_modules')) {
            return true;
        }
        
        // module.resource 包含模块名的完整路径
        if(module.resource && module.resource.endsWith('util.js')) {
            return true;
        }
        
        // count 为模块被引用的次数
        if(count > 5) {
            return true;
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# hash与长效缓存

CommonsChunkPlugin提取公共模块时,提取后的资源内部不仅仅是模块的代码,往往还包含webpack的运行时。指的是初始化环境的代码,如创建模块缓存对象、声明模块加载函数等。

早期webpack中,运行时内部也包含模块id,并且这个id是以数字不断累加的。这会导致一个问题,模块id的改变会导致运行时内部的代码发生变化,进一步影响chunk hash的生成。一般会使用chunk hash作为资源的版本号优化客户端缓存,版本号改变会导致用户频繁的更新资源,即使它们的内容并没有发生变化也会更新。。

所以可以将运行时代码单独提取出来。

const webpack = require('webpack');
module.exports = {
    entry: {
        app: './app.js',
        vendor: ['react']
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor', // 公共chunk的名字
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest', // 公共chunk的名字
        }) 
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Asset Size Chunks Chunk Names
vendor.js 69.3 kb 0 vendor
app.js 506 bytes 1 app
manifest.js 3.84 kb 2 manifest

manifest的CommonsChunkPlugin必须出现在最后,否则Webpack将无法正常提取模块。

我们页面中manifest.js应该最先被引入,用来初始化webpack环境。通过这种方式,app.js中的变化将只会影响manifest.js,而它是一个很小的文件,vendor.js内容及hash都不会变化,因此可以被用户所缓存。

# CommonsChunkPlugin的不足

  1. 一个CommonsChunkPlugin只能提取一个vendor,如果想提取多个vendor则需要配置多个插件,会增多重复的配置代码。
  2. manifest实际上会使浏览器多加载一个资源,对于页面渲染速度不是友好的。
  3. 由于内部设计缺陷,在提取公共模块的时候会破坏掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。比如在异步Chunk的场景下CommonsChunkPlugin并不会按照预期正常工作。
// webpack.config.js
const webpack = require('webpack');
module.exports = {
    entry: {
        foo: './foo.js'
    },
    output: {
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'commons', // 公共chunk的名字
            filename: 'commons.js'
        })
    ]
}


// foo.js
import React from 'react';
import ('./bar.js');

// bar.js
import React from 'react';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

结果如下:

Asset Size Chunks Chunk Names
0.foo.js 503 bytes 0
foo.js 69.8 kb 1 main
commons.js 5.78 kb 2 commons

# optimization.SplitChunks

它是webpack4为了改进CommonsChunkPlugin重新设计和实现的代码分片特性。

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
}


// foo.js
import React from 'react';
import ('./bar.js');

// bar.js
import React from 'react';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

chunks: 'all'含义是SplitChunks将会对所有的chunks生效(默认情况下,SplitChunks只对异步chunks生效,并且不需要配置)

Asset Size Chunks Chunk Names
foo.js 8.95 kb main main
0.foo.js 723 bytes 0
vendors~main.foo.js 61.1 kb vendors~main vendors~main

0.foo.js是异步加载bar.js的结果,vendors~main.foo.js是foo.js和0.foo.js提取出的react的公共模块。

# splitChunks默认的提取条件

  • 提取后的chunk可被共享或者来自node_modules目录。
  • 提取后的js chunk体积大于30kb, css chunk体积大于50kb。
  • 在按需加载过程中,并行请求的资源最大值小于等于5。按需加载是通过动态插入script标签的方式加载脚本。因为每一个请求都要花费建立连接和释放连接的成本,因此提取规则只在请求不多的时候生效。
  • 首次加载时,并行请求的资源数最大值小于等于3。

示例:

  • react是node_modules模块。
  • react体积大于30kb。
  • 按需加载时并行请求数为1,0.foo.js。
  • 首次加载时并行请求数为2,foo.js和vendors~main.foo.js

# 默认的异步提取

SplitChunks不需要配置也能生效,但仅仅针对异步资源。

module.exports = {
    entry: './foo.js',
    output: {
        filename: 'foo.js'
    }
}


// foo.js
import ('./bar.js');

// bar.js
import React from 'react';
1
2
3
4
5
6
7
8
9
10
11
12
13
Asset Size Chunks Chunk Names
foo.js 8.95 kb main main
0.foo.js 723 bytes 0
1.foo.js 61.1 kb 1

按需加载时并行请求数为2,0.foo.js和1.foo.js .
首次加载时并行请求数为1,foo.js。1.foo.js是异步加载的,所以不算。

# 配置

splitChunks默认配置:

splitChunks: {
    chunks: 'async', // initial 只对入口chunk生效
    minSize: {
        javascript: 30000,
        style: 50000
    },
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# import()

Webpack中有两种异步加载的方式:import函数和require.ensure。

通过import函数加载的模块及其依赖会被异步的进行加载,并返回一个Promise对象。

// foo.js
import('./bar.js').then(({ add }) => {
    console.log(add(2, 3))
})

// basr.js
export function add(a, b) {
    return a + b;
}
1
2
3
4
5
6
7
8
9

# 异步chunk的配置

output: {
    filename: '[name].js',
    chunkFilename: '[name].js'
}

// foo.js
import(/* webpackChunkName: "bar" */ './bar.js').then(({add}) => {
    console.log(add(2, 3));
})
1
2
3
4
5
6
7
8
9